/**
 * File: FileAccessPool.java
 * Created by: mhaimel
 * Created on: 10 Aug 2009
 * CVS:  $Id: FileAccessPool.java,v 1.3 2009/12/07 10:10:53 mhaimel Exp $
 */
package uk.ac.ebi.curtain.util.file.impl;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.pool.KeyedObjectPool;
import org.apache.commons.pool.impl.GenericKeyedObjectPool;

import uk.ac.ebi.curtain.util.file.FileAccessService;
import uk.ac.ebi.curtain.util.file.FileChannelAccess;
import uk.ac.ebi.curtain.utils.CurtainUncheckedException;
import uk.ac.ebi.curtain.utils.io.impl.BufferedRafReader;

import com.google.inject.Singleton;

/**
 * @author mhaimel
 *
 */
@Singleton
public class FileAccessPool implements FileAccessService {
	private final Log log = LogFactory.getLog(this.getClass());

	public static final int DEFAULT_READ_MAX_ACTIVE = 20;
	public static final int DEFAULT_READ_MAX_TOTAL = 100;
	public static final int DEFAULT_READ_MAX_IDLE = 1;
	public static final int DEFAULT_READ_MIN_IDLE_TIME = 300;
	public static final int DEFAULT_READ_IDLE_TIME = (int) TimeUnit.SECONDS.toMillis(10);
	public static final int DEFAULT_READ_MAX_WAIT_TIME = (int) TimeUnit.MINUTES.toMillis(2l);
	
	public static final int DEFAULT_WRITE_MAX_ACTIVE = 1;
	public static final int DEFAULT_WRITE_MAX_TOTAL = 100;
	public static final int DEFAULT_WRITE_MAX_IDLE = 1;
	public static final int DEFAULT_WRITE_MIN_IDLE_TIME = 0;
	public static final int DEFAULT_WRITE_IDLE_TIME = (int) TimeUnit.SECONDS.toMillis(1);
	public static final int DEFAULT_WRITE_MAX_WAIT_TIME = 60000;
	
	public static enum ChannelType {RAF_Buff,PrintWriter,BufferedReader,OutputStream,WritableByteChannel};
	public static void main(String[] args) {
		System.out.println(TimeUnit.MINUTES.toMillis(2l));
	}

	private final AtomicReference<KeyedObjectPool> readPool = new AtomicReference<KeyedObjectPool>();
	private final AtomicReference<KeyedObjectPool> writePool = new AtomicReference<KeyedObjectPool>();
	private final AtomicBoolean isClosed = new AtomicBoolean(false);

	public FileAccessPool() {
		GenericKeyedObjectPool.Config readConf = new GenericKeyedObjectPool.Config();
		readConf.maxActive = DEFAULT_READ_MAX_ACTIVE;
		readConf.maxTotal = DEFAULT_READ_MAX_TOTAL;
		readConf.maxIdle = DEFAULT_READ_MAX_IDLE;
		readConf.minEvictableIdleTimeMillis = DEFAULT_READ_MIN_IDLE_TIME;
		readConf.timeBetweenEvictionRunsMillis = DEFAULT_READ_IDLE_TIME;
		readConf.maxWait=DEFAULT_READ_MAX_WAIT_TIME;
		readConf.whenExhaustedAction=GenericKeyedObjectPool.WHEN_EXHAUSTED_BLOCK;
		
		readPool.set(new GenericKeyedObjectPool(new DelegatingChannelFactory(true),readConf));
		
		GenericKeyedObjectPool.Config writeConf = new GenericKeyedObjectPool.Config();
		writeConf.maxActive = DEFAULT_WRITE_MAX_ACTIVE;
		writeConf.maxTotal = DEFAULT_WRITE_MAX_TOTAL;
		writeConf.maxIdle = DEFAULT_WRITE_MAX_IDLE;
		writeConf.minEvictableIdleTimeMillis = DEFAULT_WRITE_MIN_IDLE_TIME;
		writeConf.timeBetweenEvictionRunsMillis = DEFAULT_WRITE_IDLE_TIME;
		writeConf.maxWait=DEFAULT_WRITE_MAX_WAIT_TIME;
		writeConf.whenExhaustedAction=GenericKeyedObjectPool.WHEN_EXHAUSTED_BLOCK;
		
		writePool.set(new GenericKeyedObjectPool(new DelegatingChannelFactory(false),writeConf));
	}
	
	private ChannelWrapper _borrowObject(FilePoolInfo info){
		if(isClosed.get()){
			throw new CurtainUncheckedException("FileAccess Closed!");
		}

		try {
			KeyedObjectPool pool = null;
			if(info.getRead()){
				pool = getReadPool();
			} else {
				pool = getWritePool();
			}
			ChannelWrapper channel = (ChannelWrapper) pool.borrowObject(info);
			if(!info.getRead() && getLog().isTraceEnabled()){
				getLog().trace("Borrow " + info + " from " + pool + " using " + this + " --> " + channel);
			}
			return channel;
		} catch (Exception e) {
			throw new CurtainUncheckedException("Problems open channel: " + info,e);
		}
	}
	
	private KeyedObjectPool _getPool(FilePoolInfo info){
		if(info.getRead()){
			return getReadPool();
		} else {
			return getWritePool();
		}
	}
	
	private void _invalidateChannel(FilePoolInfo info, ChannelWrapper channel) {
		if(null != channel){
			try {
				KeyedObjectPool pool = null;
				if(info.getRead()){
					pool = getReadPool();
				} else {
					pool = getWritePool();
				}
				if(!info.getRead() && getLog().isTraceEnabled()){
					getLog().trace("Invalidate Channel " + info+ " from " + pool + " using " + this + " --> " + channel);
				}
				pool.invalidateObject(info, channel);
//				_getPool(info).invalidateObject(info, channel);
//				if(info.getRead()){
//					getReadPool().invalidateObject(info, channel);
//				} else {
//					getWritePool().invalidateObject(info, channel);
//				}
			} catch (Exception e) {
				getLog().error("Problems invalidating channel: " + info,e);
			}
		}
	}	
	
	private Log getLog() {
		return log;
	}

	private void _returnChannel(FilePoolInfo info,ChannelWrapper channel){
		try {
			if(!info.getRead() && getLog().isTraceEnabled()){
				getLog().trace("Return " + info + " --> " + channel);
			}
			if(null != channel){
				_getPool(info).returnObject(info, channel);
			}
		} catch (Exception e) {
			throw new CurtainUncheckedException("Problems returning channel to pool: " + info,e);
		}
	}

//	public BufferedRafReader getBufferedRafReaderOld(File file) {
//		return _borrowObject(new FilePoolInfo(file,ChannelType.RAF_Buff,true));
//	}	
//
//	public void release(BufferedRafReader channel, File file) {
//		_returnChannel(new FilePoolInfo(file,ChannelType.RAF_Buff,true),channel);
//	}
	
	private KeyedObjectPool getReadPool() {
		return readPool.get();
	}	
	private KeyedObjectPool getWritePool() {
		return writePool.get();
	}

	public void closeAll() {
		try {
			getWritePool().clear();
		}catch (Exception e) {
			getLog().error("Problems clearing write channel!",e);
		}
		try {
			getReadPool().clear();
		}catch (Exception e) {
			getLog().error("Problems clearing read channel!",e);
		}
	}

//	public void problemWith(BufferedRafReader channel, File file) {
//		_invalidateChannel(new FilePoolInfo(file,ChannelType.RAF_Buff,true),channel);
//	}

	public FileChannelAccess<BufferedRafReader> getBufferedRafReader(File file) {
		return new GenericAccess<BufferedRafReader>(new FilePoolInfo(file,ChannelType.RAF_Buff,true));
	}
	public FileChannelAccess<PrintWriter> getPrintWriter(File file) {
		return new GenericAccess<PrintWriter>(new FilePoolInfo(file,ChannelType.PrintWriter,false));
	}

	public FileChannelAccess<BufferedReader> getBufferedReader(File file) {
		return new GenericAccess<BufferedReader>(new FilePoolInfo(file,ChannelType.BufferedReader,true));
	}

	@Override
	public FileChannelAccess<OutputStream> getOutputStream(File file) {
		return new GenericAccess<OutputStream>(new FilePoolInfo(file,ChannelType.OutputStream,false));
	}

	@Override
	public FileChannelAccess<WritableByteChannel> getWritableByteChannel(File file) {
		return new GenericAccess<WritableByteChannel>(new FilePoolInfo(file,ChannelType.WritableByteChannel,false));
	}

	public class GenericAccess<T extends Closeable> implements FileChannelAccess<T> {
		private Log _localLog = LogFactory.getLog(this.getClass());
		private FilePoolInfo info;
		private ChannelWrapper cWrapper = null;
		public GenericAccess(FilePoolInfo info) {
			this.info = info;
		}

		public T getChannel() throws IOException {
			_ensureOpen();
			return (T) cWrapper.getChannel();
		}

		public void reportProblem() {
			if(null != cWrapper){
				_invalidateChannel(info, cWrapper);
			}
			cWrapper = null;
		}

		public void close() throws IOException {
			if(null != cWrapper){
				try {
					_returnChannel(info, cWrapper);
				} catch (Exception e) {
					throw new IOException(e);
				}
			}
			cWrapper = null;
		}
		
		private Log getLocalLog() {
			return _localLog;
		}
		
		void _ensureOpen(){
			if(null == cWrapper){
				cWrapper = _borrowObject(info); 
			}
		}

		public void open() throws IOException {
			_ensureOpen();
		}

		@Override
		public boolean isOpen() {
			return null != cWrapper;
		}		
	}

	
}
